<?php

/**
 * @copyright Michiel Hakvoort 2010
 * @license http://www.opensource.org/licenses/bsd-license.php New BSD
 * @package mangrove
 * @subpackage grove
 * @filesource
 */

/*
 * Copyright (c) 2010 Michiel Hakvoort
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. The name of the author may not be used to endorse or promote products
 *    derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 */

namespace mg;

use ArrayAccess;

/**
 * Properties allows for simple access to read-only key value pairs. The class uses a {@link Storage} for caching
 * key value pairs derived from {@link PropertySource PropertySources}. Whenever a {@link PropertySource} is updated,
 * the internal cache will be updated as well.
 *
 * @author Michiel Hakvoort <michiel@hakvoort.it>
 * @version 1.0
 * @see \mg\PropertySource
 * @see \mg\StorageManager
 */
class Properties implements ArrayAccess {

    /**
     * The backing storage.
     *
     * @var \mg\Storage
     */
    private $storage = null;

    /**
     * @var \mg\Storage
     */
    private $properties = null;

    /**
     * @var int
     */
    private $os = null;

    /**
     * @var int
     */
    private $runtimeMode = null;

    /**
     * Create a new \mg\Properties. The constructor creates a cached storage.
     *
     * @param $resources array of PropertySource
     */
    public function __construct($resources = null) {
        $storageManager = in(function(StorageManager $storageManager) { return $storageManager; });
        $runtime = in(function(Runtime $runtime) { return $runtime; });

        $storage = $storageManager->getStorage('properties.storage', true);
        $this->storage = $storageManager->cache($storage);

        $properties = $storageManager->getStorage('properties.properties', true);
        $this->properties = $storageManager->cache($properties);

        $this->runtimeMode = $runtime->getMode();

        if($resources !== null && $runtime->isTransactional()) {
            $this->os = $runtime->getOS();

            $oldKnownProperties = isset($this->properties['knownProperties']) ? unserialize($this->properties['knownProperties']) : array();

            $knownProperties = array();
            // use same disambiguation step as in architecture

            // collect old properties
            // unset old properties not seen in property file
            // overwrite conflicting properties
            // tada



            /* @var $propertyResource PropertyResource */
            foreach($resources as $propertyResource) {
                $properties = $propertyResource->getProperties();

                foreach($properties as $key => $value) {
                    $key = mb_strtolower($key);

                    if(!isset($this->storage[$key]) || $this->storage[$key] !== $value) {
                        $this->storage[$key] = $value;
                    }

                    $knownProperties[$key] = $value;

                    unset($oldKnownProperties[$key]);
                }

            }

            foreach($oldKnownProperties as $key => $value) {
                unset($this->storage[$key]);
            }

            $oldKnownProperties = array();

            foreach($knownProperties as $key => $value) {
                $oldKnownProperties[$key] = true;
            }

            // Update the currently known classes
            $this->properties['knownProperties'] = serialize($oldKnownProperties);
        }
    }

    /**
     *
     * @param string $offset
     * @return bool
     */
    public function offsetExists($offset) {
        return $this->hasProperty($offset);
    }

    /**
     *
     * @param string $offset
     * @return string
     */
    public function offsetGet($offset) {
        return $this->getProperty($offset);
    }

    /**
     *
     * @param string $offset
     * @param string $value
     */
    public function offsetSet($offset, $value) {
        throw new Exception('mg\Properties is read-only');
    }

    /**
     *
     * @param string $offset
     */

    public function offsetUnset($offset) {
        throw new Exception('mg\Properties is read-only');
    }

    /**
     * Check whether a property has been set.
     *
     * @access public
     * @param string $property The property to check
     * @return boolean True when the property has been set
     */
    public function hasProperty($property) {
        if($this->runtimeMode === Runtime :: MODE_PRODUCTION) {
            return isset($this->storage[$property]);
        }

        if($this->os === Runtime :: OS_WIN) {
            if(isset($_SERVER['USERNAME'])) {
                $username = trim(mb_strtolower($_SERVER['USERNAME']), '$');
            } elseif(isset($_SERVER['COMPUTERNAME'])) {
                $username = trim(mb_strtolower($_SERVER['COMPUTERNAME']), '$');
            } else {
                $username = get_current_user();
            }
        } else {
            $username = get_current_user();
        }

        $modes = array(Runtime :: MODE_DEVELOPMENT => 'mode_development'
        ,Runtime :: MODE_TESTING     => 'mode_testing'
        ,Runtime :: MODE_ACCEPTANCE  => 'mode_acceptance');

        $modeName = $modes[$this->runtimeMode];

        $result = isset($this->storage["{$modeName}&{$username}&{$property}"]);

        if($result === false) {
            $result = isset($this->storage["{$modeName}&{$property}"]);
        }

        if($result === false) {
            $result = isset($this->storage["{$username}&{$property}"]);
        }

        if($result === false) {
            $result = isset($this->storage[$property]);
        }

        return $result;
    }

    /**
     * Get the value of a single property.
     *
     * @param string $property The property to retrieve
     * @return string The value of the propertyunknown
     */
    public function getProperty($property) {
        if($this->runtimeMode === Runtime :: MODE_PRODUCTION) {
            return $this->storage[$property];
        }

        if($this->os === Runtime :: OS_WIN) {
            if(isset($_SERVER['USERNAME'])) {
                $username = trim(mb_strtolower($_SERVER['USERNAME']), '$');
            } elseif(isset($_SERVER['COMPUTERNAME'])) {
                $username = trim(mb_strtolower($_SERVER['COMPUTERNAME']), '$');
            } else {
                $username = get_current_user();
            }
        } else {
            $username = get_current_user();
        }

        $modes = array(Runtime :: MODE_DEVELOPMENT => 'mode_development'
        ,Runtime :: MODE_TESTING     => 'mode_testing'
        ,Runtime :: MODE_ACCEPTANCE  => 'mode_acceptance');

        $modeName = $modes[$this->runtimeMode];

        $result = $this->storage["{$modeName}&{$username}&{$property}"];

        if($result === false) {
            $result = $this->storage["{$modeName}&{$property}"];
        }

        if($result === false) {
            $result = $this->storage["{$username}&{$property}"];
        }

        if($result === false) {
            $result = $this->storage[$property];
        }

        return $result;
    }


}

/**
 * PropertySource acts as a source of key value pairs. The PropertySource is used by {@link \mg\Properties} in order
 * to create a set of properties.
 *
 * @author Michiel Hakvoort <michiel@hakvoort.it>
 * @version 1.0
 * @see \mg\Properties
 */
interface PropertySource {

    /**
     * Get the array of key value pairs defining the properties in this PropertySource.
     *
     * @access public
     * @return array The array of properties
     */
    public function getProperties();

}

/**
 * The FileSystemPropertySource acts as a {@link PropertySource}, getting properties from property files.
 * A single FileSystemPropertySource parses properties from an entire directory of property files. Duplicate
 * properties defined in several files might result in ambiguous behaviour. A property file is a file with the filename
 * ending in the extension ".properties". Furthermore, the property file has the same format as the {@link http://java.sun.com/javase/6/docs/api/java/util/Properties.html#load(java.io.Reader) Java property file format}.
 * To prevent property files to be parsed every time, only modified files are being passed. This functionality is offered by the {@link \mg\FileSystem}.
 *
 * @author Michiel Hakvoort <michiel@hakvoort.it>
 * @version 1.0
 * @see \mg\Properties
 * @see \mg\PropertySource
 * @see \mg\FileSystem
 * @link http://java.sun.com/javase/6/docs/api/java/util/Properties.html#load(java.io.Reader)
 */
class FileSystemPropertySource implements PropertySource {

    /**
     * The directory to search for property files
     *
     * @var string
     */
    private $path = null;

    /**
     * The parsed properties
     *
     * @var array
     */
    private $properties = array();

    /**
     * Create a new FileSystemPropertySource for the given configuration directory and parse the
     * property files contained within the configuration directory.
     *
     * @param string $path The configuration directory to recursively scan for property files
     */
    public function __construct($path) {
        $canonicalPath = realpath($path);

        if($canonicalPath === false) {
            throw new Exception("Path \"{$path}\" does not exist");
        }

        $this->path = $canonicalPath;
    }

    /**
     * {@inheritDoc}
     */
    public function getProperties() {
        $path = $this->path;
        $fileSystemResource = $this;

        return in(function(StorageManager $storageManager, FileSystem $fileSystem) use($path, $fileSystemResource) {

            $modeMap = array('testing' => 'mode_testing', 'development' => 'mode_development', 'acceptance' => 'mode_acceptance');

            $parsedFiles = $storageManager->getStorage('filesystemresource.parsedfiles', false);
            $cache = $storageManager->getStorage('filesystemresource.cache', false);

            $iterator = new \RecursiveIteratorIterator(
            new RegexRecursiveDirectoryFilterIterator(
            new \RecursiveDirectoryIterator($path, \RecursiveDirectoryIterator :: CURRENT_AS_FILEINFO)
            ,'#^[^.].*$#i'
            ,'#^[^.].*\.properties$#i'));

            $result = array();

            $base64Path = base64_encode($path);

            $knownFiles = array();

            if(isset($parsedFiles[$base64Path])) {
                $knownFiles = unserialize($parsedFiles[$base64Path]);
            }

            $newParsedFiles = array();

            foreach($iterator as $filename => $file) {
                if(!$file->isReadable()) {
                    continue;
                }

                $isAnnotated = preg_match_all('#(?:$|\.|\])([um])\[(.*?)\]#i', $filename, $fileAnnotations, PREG_SET_ORDER) > 0;

                $encodedFilename = base64_encode($filename);

                unset($knownFiles[$encodedFilename]);

                $fileStatus = $fileSystem->getFileStatus($filename);

                $properties = null;

                if($fileStatus === FileSystem :: FILE_NOT_MODIFIED) {
                    if(isset($cache[$encodedFilename])) {
                        $properties = unserialize($cache[$encodedFilename]);
                    }
                }

                if($properties === null) {
                    $parser = new PropertiesFileParser(file_get_contents($filename));

                     
                    if($parser->match_File() === false) {
                        throw new \mg\Exception("Could not parse {$filename}");
                    }

                    $properties = $parser->properties;
                    $cache[$encodedFilename] = serialize($properties);
                }

                $newParsedFiles[$filename] = true;

                if($isAnnotated) {
                    $oldProperties = $properties;

                    $properties = array();

                    $users = array();
                    $modes = array();

                    foreach($fileAnnotations as $annotation) {
                        $key = $annotation[1];
                        $value = explode(',', $annotation[2]);
                        $value = array_map(function($val) { return mb_strtolower($val); }, $value);

                        switch($key) {
                            case 'u' :
                                $users = array_merge($users, $value);
                                break;
                            case 'm' :
                                foreach($value as $mode) {
                                    if(!isset($modeMap[$mode])) {
                                        throw new Exception("Unsupported mode annotation '{$mode}' for resource file '{$file}'");
                                    }
                                }
                                $modes = array_merge($modes, $value);
                            default :
                        }
                    }

                    if(empty($users)) { $users[]= null; }
                    if(empty($modes)) { $modes[]= null; }

                    foreach($oldProperties as $key => $value) {
                        foreach($modes as $mode) {
                            $prefix = $mode !== null ? ($modeMap[$mode] . '&') : '';

                            foreach($users as $user) {
                                $newKey = $prefix . ($user !== null ? ($user . '&') : '') . $key;
                                $properties[$newKey] = $value;
                            }
                        }
                    }
                }

                $result = $properties + $result;
            }

            foreach($knownFiles as $knownFile => $value) {
                unset($cache[$knownFile]);
            }

            $parsedFiles[$base64Path] = serialize($newParsedFiles);

            return $result;
        });
    }

}